Passed
Push — main ( 37c54c...c55404 )
by Yuri
01:25
created

ReceiptParser   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 113
Duplicated Lines 0 %

Test Coverage

Coverage 97.78%

Importance

Changes 0
Metric Value
wmc 20
eloc 89
dl 0
loc 113
ccs 44
cts 45
cp 0.9778
c 0
b 0
f 0
rs 10

14 Functions

Rating   Name   Duplication   Size   Complexity  
A extractSequencesFromContent 0 5 1
A removeDuplicates 0 3 1
A validateParsedFields 0 7 2
A addToArrayFieldIfApplicable 0 10 2
A addFieldToReceipt 0 4 1
A parseReceipt 0 10 1
A createInitialParsedReceipt 0 6 1
A parseReceiptContent 0 4 1
A processSequence 0 5 2
A deduplicateArrayFields 0 4 1
A extractStringValue 0 9 2
A handleVerifiedSequence 0 7 1
A getFieldHandler 0 12 3
A isValidReceiptFieldKey 0 3 1
1 1
import * as ASN1 from 'asn1js'
2
3 1
import { RECEIPT_FIELDS_MAP, ReceiptFieldsKeyNames, ReceiptFieldsKeyValues } from './mappings'
4 1
import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID, IN_APP } from './constants'
5 1
import { verifyFieldSchema, verifyReceiptSchema } from './verifications'
6
7
export type Environment = 'Production' | 'ProductionSandbox' | string
8
9
export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
10
  ENVIRONMENT: Environment
11
  IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
12
  IN_APP_TRANSACTION_IDS: string[]
13
}
14
15
class ReceiptParser {
16
  private readonly parsed: ParsedReceipt
17
18
  constructor() {
19 3
    this.parsed = this.createInitialParsedReceipt()
20
  }
21
22
  public parseReceipt(receipt: string): ParsedReceipt {
23 3
    const rootSchemaVerification = verifyReceiptSchema(receipt)
24 2
    const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
25
26 2
    this.parseReceiptContent(content)
27 2
    this.validateParsedFields()
28 2
    this.deduplicateArrayFields()
29
30 2
    return this.parsed
31
  }
32
33
  private createInitialParsedReceipt(): ParsedReceipt {
34 3
    return {
35
      ENVIRONMENT: 'Production',
36
      IN_APP_ORIGINAL_TRANSACTION_IDS: [],
37
      IN_APP_TRANSACTION_IDS: [],
38
    }
39
  }
40
41
  private parseReceiptContent(content: ASN1.OctetString): void {
42 10
    const sequences = this.extractSequencesFromContent(content)
43 10
    sequences.forEach(this.processSequence.bind(this))
44
  }
45
46
  private extractSequencesFromContent(content: ASN1.OctetString): ASN1.Sequence[] {
47 10
    const [contentSet] = content.valueBlock.value as ASN1.Set[]
48 10
    return contentSet.valueBlock.value
49 194
      .filter(v => v instanceof ASN1.Sequence) as ASN1.Sequence[]
50
  }
51
52
  private processSequence(sequence: ASN1.Sequence): void {
53 194
    const verifiedSequence = verifyFieldSchema(sequence)
54 194
    if (verifiedSequence) {
55 194
      this.handleVerifiedSequence(verifiedSequence)
56
    }
57
  }
58
59
  private handleVerifiedSequence(verifiedSequence: ASN1.CompareSchemaSuccess): void {
60 194
    const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
61 194
    const fieldValue = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
62
63 194
    const handler = this.getFieldHandler(fieldKey)
64 194
    handler(fieldValue)
65
  }
66
67
  private getFieldHandler(fieldKey: number): (fieldValue: ASN1.OctetString) => void {
68 194
    if (fieldKey === IN_APP) {
69 8
      return this.parseReceiptContent.bind(this)
70
    }
71 186
    if (this.isValidReceiptFieldKey(fieldKey)) {
72 88
      const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
73 88
      return (fieldValue: ASN1.OctetString) => {
74 88
        this.addFieldToReceipt(name, this.extractStringValue(fieldValue))
75
      }
76
    }
77 98
    return () => {}
78
  }
79
80
  private isValidReceiptFieldKey(value: unknown): value is ReceiptFieldsKeyValues {
81 186
    return typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues)
82
  }
83
84
  private extractStringValue(field: ASN1.OctetString): string {
85 88
    const [fieldValue] = field.valueBlock.value
86
87 88
    if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
88 68
      return fieldValue.valueBlock.value
89
    }
90
91 20
    return field.toJSON().valueBlock.valueHex
92
  }
93
94
  private addFieldToReceipt(name: ReceiptFieldsKeyNames, value: string): void {
95 88
    this.addToArrayFieldIfApplicable(name, value)
96 88
    this.parsed[name] = value
97
  }
98
99
  private addToArrayFieldIfApplicable(name: ReceiptFieldsKeyNames, value: string): void {
100 88
    const arrayFields: Record<string, keyof ParsedReceipt> = {
101
      'IN_APP_ORIGINAL_TRANSACTION_ID': 'IN_APP_ORIGINAL_TRANSACTION_IDS',
102
      'IN_APP_TRANSACTION_ID': 'IN_APP_TRANSACTION_IDS',
103
    }
104
105 88
    const arrayFieldName = arrayFields[name]
106 88
    if (arrayFieldName) {
107 16
      (this.parsed[arrayFieldName] as string[]).push(value)
108
    }
109
  }
110
111
  private validateParsedFields(): void {
112 2
    const missingFields = Array.from(RECEIPT_FIELDS_MAP.values())
113 34
      .filter(fieldKey => !(fieldKey in this.parsed))
114
115 2
    if (missingFields.length > 0) {
116
      throw new Error(`Missing required fields: ${missingFields.join(', ')}`)
117
    }
118
  }
119
120
  private deduplicateArrayFields(): void {
121 2
    this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS)
122 2
    this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS)
123
  }
124
125
  private removeDuplicates(array: string[]): string[] {
126 4
    return [...new Set(array)]
127
  }
128
}
129
130 1
export function parseReceipt(receipt: string): ParsedReceipt {
131 3
  return new ReceiptParser().parseReceipt(receipt)
132
}
133